/*
 * Copyright (c) 2018 Intel Corporation.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define LOG_MODULE_NAME net_tc_app
#define NET_LOG_LEVEL LOG_LEVEL_DBG

#include <zephyr.h>
#include <errno.h>

#include <net/net_core.h>
#include <net/net_l2.h>
#include <net/net_if.h>
#include <net/ethernet.h>
#include <net/net_context.h>
#include <net/net_app.h>

#define MY_PORT 0
#define PEER_PORT 4242

#define WAIT_TIME  K_SECONDS(2)
#define CONNECT_TIME  K_SECONDS(10)

#if defined(CONFIG_NET_IPV6)
static struct net_app_ctx udp6[NET_TC_COUNT];
#endif
#if defined(CONFIG_NET_IPV4)
static struct net_app_ctx udp4[NET_TC_COUNT];
#endif

static struct k_sem quit_lock;

/* Generated by http://www.lipsum.com/
 * 3 paragraphs, 176 words, 1230 bytes of Lorem Ipsum
 */
const char lorem_ipsum[] =
	"Lorem ipsum dolor sit amet, consectetur adipiscing elit. "
	"Vestibulum id cursus felis, sit amet suscipit velit. Integer "
	"facilisis malesuada porta. Nunc at accumsan mauris. Etiam vehicula, "
	"arcu consequat feugiat venenatis, tellus velit gravida ligula, quis "
	"posuere sem leo eget urna. Curabitur condimentum leo nec orci "
	"mattis, nec faucibus dui rutrum. Ut mollis orci in iaculis "
	"consequat. Nulla volutpat nibh eu velit sagittis, a iaculis dui "
	"aliquam."
	"\n"
	"Quisque interdum consequat eros a eleifend. Fusce dapibus nisl "
	"sit amet velit posuere imperdiet. Quisque accumsan tempor massa "
	"sit amet tincidunt. Integer sollicitudin vehicula tristique. Nulla "
	"sagittis massa turpis, ac ultricies neque posuere eu. Nulla et "
	"imperdiet ex. Etiam venenatis sed lacus tincidunt hendrerit. In "
	"libero nisl, congue id tellus vitae, tincidunt tristique mauris. "
	"Nullam sed porta massa. Sed condimentum sem eu convallis euismod. "
	"Suspendisse lobortis purus faucibus, gravida turpis id, mattis "
	"velit. Maecenas eleifend sapien eu tincidunt lobortis. Sed elementum "
	"sapien id enim laoreet consequat."
	"\n"
	"Aenean et neque aliquam, lobortis lectus in, consequat leo. Sed "
	"quis egestas nulla. Quisque ac risus quis elit mollis finibus. "
	"Phasellus efficitur imperdiet metus."
	"\n";

static int ipsum_len = sizeof(lorem_ipsum) - 1;

struct stats {
	u32_t sent;
	u32_t received;
	u32_t dropped;
	u32_t wrong_order;
	u32_t invalid;

	u32_t sent_time_sum;
	u32_t sent_time_count;
	s64_t sent_time; /* in milliseconds */
};

struct configs;

struct data {
	/* Work controlling udp data sending */
	struct k_delayed_work recv;
	struct net_app_ctx *udp;
	struct configs *conf;
	sa_family_t family;

	const char *proto;
	u32_t expecting_udp;
	u8_t priority;

	struct stats stats;
};

struct configs {
	struct data ipv4[NET_TC_COUNT];
	struct data ipv6[NET_TC_COUNT];
};

static struct configs conf = {
	.ipv4 = {
		[0 ... (NET_TC_COUNT - 1)] = {
			.proto = "IPv4",
			.family = AF_INET,
		}
	},
	.ipv6 = {
		[0 ... (NET_TC_COUNT - 1)] = {
			.proto = "IPv6",
			.family = AF_INET6,
		}
	}
};

#define TYPE_SEQ_NUM 42

struct header {
	u8_t type;
	u8_t len;
	union {
		u8_t value[0];
		struct {
			u32_t seq;
			s64_t sent;
		};
	};
} __packed;

#if CONFIG_NET_VLAN_COUNT > 1
#define CREATE_MULTIPLE_TAGS
#endif

struct ud {
	struct net_if *first;
	struct net_if *second;
};

#if defined(CREATE_MULTIPLE_TAGS)
static void iface_cb(struct net_if *iface, void *user_data)
{
	struct ud *ud = user_data;

	if (net_if_l2(iface) != &NET_L2_GET_NAME(ETHERNET)) {
		return;
	}

	if (iface == ud->first) {
		return;
	}

	ud->second = iface;
}
#endif

static int init_app(void)
{
	struct net_if *iface;
	int ret;

#if defined(CREATE_MULTIPLE_TAGS)
	struct net_if_addr *ifaddr;
	struct in_addr addr4;
	struct in6_addr addr6;
	struct ud ud;
#endif

	iface = net_if_get_first_by_type(&NET_L2_GET_NAME(ETHERNET));
	if (!iface) {
		NET_ERR("No ethernet interfaces found.");
		return -ENOENT;
	}

#if defined(CONFIG_NET_VLAN)
	ret = net_eth_vlan_enable(iface, CONFIG_SAMPLE_VLAN_TAG);
	if (ret < 0) {
		NET_ERR("Cannot enable VLAN for tag %d (%d)",
			CONFIG_SAMPLE_VLAN_TAG, ret);
	}
#endif

#if defined(CREATE_MULTIPLE_TAGS)
	ud.first = iface;
	ud.second = NULL;

	net_if_foreach(iface_cb, &ud);

	/* This sample has two VLANs. For the second one we need to manually
	 * create IP address for this test. But first the VLAN needs to be
	 * added to the interface so that IPv6 DAD can work properly.
	 */
	ret = net_eth_vlan_enable(ud.second, CONFIG_SAMPLE_VLAN_TAG_2);
	if (ret < 0) {
		NET_ERR("Cannot enable VLAN for tag %d (%d)",
			CONFIG_SAMPLE_VLAN_TAG_2, ret);
	}

#if defined(CONFIG_NET_IPV6)
	if (net_addr_pton(AF_INET6, CONFIG_SAMPLE_IPV6_ADDR_2, &addr6)) {
		NET_ERR("Invalid address: %s", CONFIG_SAMPLE_IPV6_ADDR_2);
		return -EINVAL;
	}

	ifaddr = net_if_ipv6_addr_add(ud.second, &addr6, NET_ADDR_MANUAL, 0);
	if (!ifaddr) {
		NET_ERR("Cannot add %s to interface %p",
			CONFIG_SAMPLE_IPV6_ADDR_2, ud.second);
		return -EINVAL;
	}
#else
	ARG_UNUSED(addr6);
#endif /* IPV6 */

#if defined(CONFIG_NET_IPV4)
	if (net_addr_pton(AF_INET, CONFIG_SAMPLE_IPV4_ADDR_2, &addr4)) {
		NET_ERR("Invalid address: %s", CONFIG_SAMPLE_IPV4_ADDR_2);
		return -EINVAL;
	}

	ifaddr = net_if_ipv4_addr_add(ud.second, &addr4, NET_ADDR_MANUAL, 0);
	if (!ifaddr) {
		NET_ERR("Cannot add %s to interface %p",
			CONFIG_SAMPLE_IPV4_ADDR_2, ud.second);
		return -EINVAL;
	}
#else
	ARG_UNUSED(addr4);
#endif /* IPV4 */

#endif

	return ret;
}

static u32_t calc_time(u32_t count, u32_t sum)
{
	if (!count) {
		return 0;
	}

	return (sum * 1000) / count;
}

#define PRINT_STATISTICS_INTERVAL (30 * MSEC_PER_SEC)

static void stats(struct data *data)
{
	static bool first = true;
	static s64_t next_print;
	s64_t curr = k_uptime_get();

	if (!next_print || (next_print < curr &&
	    (!((curr - next_print) > PRINT_STATISTICS_INTERVAL)))) {
		s64_t new_print;
		int i;

		if (first) {
			first = false;
			goto skip_print;
		}

		NET_INFO("Traffic class statistics:");
		NET_INFO("   Prio\tSent\tRecv\tDrop\tMiss\tTime (us)");

#if defined(CONFIG_NET_IPV6)
		for (i = 0; i < NET_TC_COUNT; i++) {
			u32_t round_trip_time =
				calc_time(
				     data->conf->ipv6[i].stats.sent_time_count,
				     data->conf->ipv6[i].stats.sent_time_sum);

			NET_INFO("v6 %d\t%u\t%u\t%u\t%u\t%u",
				 data->conf->ipv6[i].priority,
				 data->conf->ipv6[i].stats.sent,
				 data->conf->ipv6[i].stats.received,
				 data->conf->ipv6[i].stats.dropped,
				 data->conf->ipv6[i].stats.wrong_order,
				 round_trip_time);
		}
#endif

#if defined(CONFIG_NET_IPV4)
		for (i = 0; i < NET_TC_COUNT; i++) {
			u32_t round_trip_time =
				calc_time(
				     data->conf->ipv4[i].stats.sent_time_count,
				     data->conf->ipv4[i].stats.sent_time_sum);

			NET_INFO("v4 %d\t%u\t%u\t%u\t%u\t%u",
				 data->conf->ipv4[i].priority,
				 data->conf->ipv4[i].stats.sent,
				 data->conf->ipv4[i].stats.received,
				 data->conf->ipv4[i].stats.dropped,
				 data->conf->ipv4[i].stats.wrong_order,
				 round_trip_time);
		}
#endif
		NET_INFO("---");

skip_print:
		new_print = curr + PRINT_STATISTICS_INTERVAL;
		if (new_print > curr) {
			next_print = new_print;
		} else {
			/* Overflow */
			next_print = PRINT_STATISTICS_INTERVAL -
				(LLONG_MAX - curr);
		}
	}
}

static struct net_pkt *prepare_send_pkt(struct net_app_ctx *ctx,
					const char *name,
					int *expecting_len,
					struct data *data)
{
	struct net_pkt *send_pkt;
	struct header *hdr;
	u32_t seq;
	s32_t timeout = K_SECONDS(1);

	send_pkt = net_app_get_net_pkt(ctx, data->family, timeout);
	if (!send_pkt) {
		return NULL;
	}

	seq = htonl(data->stats.sent + 1);

	*expecting_len = net_pkt_append(send_pkt, *expecting_len,
					lorem_ipsum, timeout);

	hdr = (struct header *)send_pkt->frags->data;
	hdr->type = TYPE_SEQ_NUM;
	hdr->len = sizeof(seq);

	UNALIGNED_PUT(seq, &hdr->seq);
	UNALIGNED_PUT(k_uptime_get(), &hdr->sent);

	return send_pkt;
}

static bool send_udp_data(struct net_app_ctx *ctx, struct data *data)
{
	s32_t timeout = K_SECONDS(1);
	struct net_pkt *pkt;
	size_t len;
	int ret;

	data->expecting_udp = sys_rand32_get() % ipsum_len;

	pkt = prepare_send_pkt(ctx, data->proto, &data->expecting_udp, data);
	if (!pkt) {
		return false;
	}

	len = net_pkt_get_len(pkt);

	NET_ASSERT_INFO(data->expecting_udp == len,
			"Data to send %d bytes, real len %zu",
			data->expecting_udp, len);

	data->stats.sent_time = k_uptime_get();

	ret = net_app_send_pkt(ctx, pkt, NULL, 0, timeout,
			       UINT_TO_POINTER(len));
	if (ret < 0) {
		net_pkt_unref(pkt);
	}

	data->stats.sent++;

	k_delayed_work_submit(&data->recv, WAIT_TIME);

	stats(data);

	return true;
}

static void send_more_data(struct net_app_ctx *ctx, struct data *data)
{
	bool ret;

	do {
		ret = send_udp_data(ctx, data);
		if (!ret) {
			/* Avoid too much flooding */
			k_sleep(K_MSEC(10));
		}
	} while (!ret);

	/* We should not call k_yield() here as that will not let lower
	 * priority thread to run.
	 */
	k_sleep(K_MSEC(1));
}

static void udp_received(struct net_app_ctx *ctx,
			 struct net_pkt *pkt,
			 int status,
			 void *user_data)
{
	struct data *data = ctx->user_data;
	struct header *hdr = (struct header *)net_pkt_appdata(pkt);

	ARG_UNUSED(user_data);
	ARG_UNUSED(status);

	if (data->expecting_udp != net_pkt_appdatalen(pkt)) {
		NET_DBG("Sent %d bytes, received %u bytes",
			data->expecting_udp, net_pkt_appdatalen(pkt));
	}

	net_pkt_unref(pkt);

	k_delayed_work_cancel(&data->recv);

	if (hdr->type != TYPE_SEQ_NUM) {
		data->stats.invalid++;
	} else {
		if (ntohl(UNALIGNED_GET(&hdr->seq)) != data->stats.sent) {
			data->stats.wrong_order++;
		} else {
			data->stats.received++;

			data->stats.sent_time_sum += k_uptime_get() -
				data->stats.sent_time;
			data->stats.sent_time_count++;
		}
	}

	send_more_data(ctx, data);
}

static int connect_udp(sa_family_t family, struct net_app_ctx *ctx,
		       const char *peer, void *user_data, u8_t priority)
{
	struct data *data = user_data;
	size_t optlen = sizeof(priority);
	int ret;

	data->udp = ctx;

	ret = net_app_init_udp_client(ctx, NULL, NULL, peer, PEER_PORT,
				      WAIT_TIME, user_data);
	if (ret < 0) {
		NET_ERR("Cannot init %s UDP client (%d)", data->proto, ret);
		goto fail;
	}

#if defined(CONFIG_NET_CONTEXT_NET_PKT_POOL)
	net_app_set_net_pkt_pool(ctx, tx_udp_slab, data_udp_pool);
#endif

	ret = net_app_set_cb(ctx, NULL, udp_received, NULL, NULL);
	if (ret < 0) {
		NET_ERR("Cannot set callbacks (%d)", ret);
		goto fail;
	}

	ret = net_app_connect(ctx, CONNECT_TIME);
	if (ret < 0) {
		NET_ERR("Cannot connect UDP (%d)", ret);
		goto fail;
	}

#if defined(CONFIG_NET_IPV4)
	if (family == AF_INET) {
		net_context_set_option(ctx->ipv4.ctx, NET_OPT_PRIORITY,
				       &priority, sizeof(u8_t));

		net_context_get_option(ctx->ipv4.ctx, NET_OPT_PRIORITY,
				       &priority, &optlen);
	}
#endif

#if defined(CONFIG_NET_IPV6)
	if (family == AF_INET6) {
		net_context_set_option(ctx->ipv6.ctx, NET_OPT_PRIORITY,
				       &priority, sizeof(u8_t));

		net_context_get_option(ctx->ipv6.ctx, NET_OPT_PRIORITY,
				       &priority, &optlen);
	}
#endif

	data->priority = priority;
fail:
	return ret;
}

static void wait_reply(struct k_work *work)
{
	/* This means that we did not receive response in time. */
	struct data *data = CONTAINER_OF(work, struct data, recv);

	data->stats.dropped++;

	/* Send a new packet at this point */
	send_more_data(data->udp, data);
}

static void setup_clients(void)
{
	int ret, i;

#if defined(CONFIG_NET_IPV6)
	for (i = 0; i < NET_TC_COUNT; i++) {
		k_delayed_work_init(&conf.ipv6[i].recv, wait_reply);

		conf.ipv6[i].conf = &conf;

		if (i % 2) {
			NET_DBG("TC %d connecting to %s", i,
				CONFIG_NET_CONFIG_PEER_IPV6_ADDR);
			ret = connect_udp(AF_INET6, &udp6[i],
					  CONFIG_NET_CONFIG_PEER_IPV6_ADDR,
					  &conf.ipv6[i], i);
		} else {
			NET_DBG("TC %d connecting to %s", i,
				CONFIG_SAMPLE_PEER_IPV6_ADDR_2);
			ret = connect_udp(AF_INET6, &udp6[i],
					  CONFIG_SAMPLE_PEER_IPV6_ADDR_2,
					  &conf.ipv6[i], i);
		}

		if (ret < 0) {
			NET_ERR("Cannot init IPv6 UDP client %d (%d)",
				i + 1, ret);
		}
	}
#endif

#if defined(CONFIG_NET_IPV4)
	for (i = 0; i < NET_TC_COUNT; i++) {
		k_delayed_work_init(&conf.ipv4[i].recv, wait_reply);

		conf.ipv4[i].conf = &conf;

		if (i % 2) {
			NET_DBG("TC %d connecting to %s", i,
				CONFIG_NET_CONFIG_PEER_IPV4_ADDR);
			ret = connect_udp(AF_INET, &udp4[i],
					  CONFIG_NET_CONFIG_PEER_IPV4_ADDR,
					  &conf.ipv4[i], i);
		} else {
			NET_DBG("TC %d connecting to %s", i,
				CONFIG_SAMPLE_PEER_IPV4_ADDR_2);
			ret = connect_udp(AF_INET, &udp4[i],
					  CONFIG_SAMPLE_PEER_IPV4_ADDR_2,
					  &conf.ipv4[i], i);
		}

		if (ret < 0) {
			NET_ERR("Cannot init IPv4 UDP client %d (%d)",
				i + 1, ret);
		}
	}
#endif

	/* We can start to send data when UDP is "connected" */
	for (i = 0; i < NET_TC_COUNT; i++) {
#if defined(CONFIG_NET_IPV6)
		send_more_data(&udp6[i], &conf.ipv6[i]);
#endif
#if defined(CONFIG_NET_IPV4)
		send_more_data(&udp4[i], &conf.ipv4[i]);
#endif
	}
}

void main(void)
{
	k_sem_init(&quit_lock, 0, UINT_MAX);

	init_app();

	/* This extra sleep is needed so that the network stabilizes a bit
	 * before we start to send data. This is important as we have multiple
	 * network interfaces and all of them should be configured properly
	 * before we continue.
	 */
	k_sleep(K_SECONDS(5));

	setup_clients();

	k_sem_take(&quit_lock, K_FOREVER);
}
